關於物件,本文會提到
建立物件有兩種方式
const obj = { name: 'Jack' };
。const obj = new Object(); obj.name = 'Jack';
,也就是原生功能,但由於一些雷區,這種方式其實很少用。簡單來說,兩者主要的差別是新增屬性時,字面值可在物件建立時一次全部加入,但建構形式必須在物件建立後一筆一筆新增。
JavaScript 的資料型態有七種-字串(string)、數字(number)、布林值(boolean)、null、undefined、物件(object)與 symbol。其中函式(function)和陣列(array)、日期(date)皆為物件的一種,function 是可呼叫的物件,而 array 是結構較嚴謹的物件。
關於型別就會想到型別檢測,想到型別檢測就不得不提一下 typeof 的議題...
typeof 'Hello World!' // 'string'
typeof true // 'boolean'
typeof 1234567 // 'number'
typeof null // 'object'
typeof undefined // 'undefined'
typeof { name: 'Jack' } // 'object'
typeof Symbol() // 'symbol'
typeof function() {} // 'function'
typeof [1, 2, 3] // 'object'
typeof NaN // 'number'
這裡會看到幾個有趣的(奇怪的)地方...
typeof null
卻得到 object,而非 null!這可說是一個 bug,可是若因為修正了這個 bug 則可能會導致很多網站壞掉,因此就不修了!typeof function() {}
是得到 function 而非 object,和陣列依舊得到 object 是不一樣的。typeof NaN
結果就是 number,不要被字面上的意思「不是數字」(not a number)給弄糊塗了。另外,NaN 與任何數字運算都會得到 NaN,並且 NaN 不大於、不小於也不等於任何數字,包含 NaN 它自己。...
...
每次看到 typeof 都很想問 JavaScript 的作者...
記這麼多東西,都累到要倒地不起了(哭
...
...
另外,如果想知道這個物件到底是屬於哪個子型別,則可使用 Object.prototype.toString
來檢視 [[Class]]
這個內部屬性。
Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
Object.prototype.toString.call({ name: 'Jack' }); // "[object Object]"
Object.prototype.toString.call(function sayHi() {}); // "[object Function]"
Object.prototype.toString.call(/helloworld/i); // "[object RegExp]"
Object.prototype.toString.call(new Date()); // "[object Date]"
不過,這個方法其實是借用建構形式其實就是物件包裹器的概念,而能使用物件型別值內的 [[Class]]
屬性來辨別這個值是屬於物件的哪個子分類。
內建物件指的是使用內建函式所建立的物件,這些物件都屬於物件子型別的一種,除了上面提到的陣列、函式與日期外,這裡列出物件所有的子型別:String、Number、Boolean、Object、Function、
Array、Date、RegExp、Error,它們的用途是給予開發者取得屬性或方法的使用,也就是我們常聽到的原生功能。因此,當使用建構式建立字串、布林或數字等值時,建立的其實不是基本型別值而是物件,因此可用 instanceof 來檢查是由哪個建構式建立,也就是來判斷是否為指定的物件型別。
如下,使用字串字面值宣告了一個字串 str,當我們使用 str 進行 .length
的操作以取得其長度時,JavaScript 就會將這個字串基本型別的值強制轉型成對應的物件子型別,也就是上面提到的 String。
const str = 'Hello World!';
str instanceof String // false
str.length // 12
const strObj = new String('Hello World!');
strObj instanceof String // true
strObj.length // 12
注意
更多關於原生功能的資訊,可參考這裡。
物件的內容是由屬性組成的,而屬性是由 key-value pair 構成,value 可為任意資料型別的值,並且值是以參考(reference)的方式(存位置)儲存。
如何存取物件的屬性呢?有兩種方式
.
[ ]
其中,特性存取 .
必須符合識別字的規範,簡單說就是只能是字母、數字、$
(錢字號)或 _
(底線),並且不能以數字開頭,之後可加上 a-z、A-Z、$
、_
和數字 0-9,可為關鍵字或保留字。
讓我們來看一些疑難雜症吧!
若要使用一些包含特殊字符或動態產生的字串作為屬性名稱,就必須使用鍵值存取 [ ]
的方式。
包含特殊字符的屬性名稱。
const obj = {
'!!12345!!': 'Hello World',
};
obj.!!12345!! // Uncaught SyntaxError: Unexpected token !
obj['!!12345!!'] // "Hello World"
ES6 新增動態產生的字串作為屬性名稱功能,讓 key 的值可經由運算得出。
const prefix = 'fresh-';
const fruits = {
[prefix + 'apple']: 100,
[prefix + 'orange']: 60,
};
fruits['fresh-apple'] // 100
fruits['fresh-orange'] // 60
屬性名稱只能是字串,若不是字串則會被強制轉為字串。
如下,obj[obj]
的 key 值被強制轉為字串 '[object Object]'
,同理,obj[999]
的 key 值 999 也被轉為字串 '999'
了。
const obj = { Qoo: '有種果汁真好喝' };
obj[obj] = '喝的時候酷兒';
obj[999] = '喝完臉紅紅!'
obj['[object Object]'] // '喝的時候酷兒'
obj['999'] // '喝完臉紅紅!'
闢謠...澄清...!!??
在其他語言中,屬於某個物件的函式稱為方法,但在 JavaScript 中,函式並不會特別屬於某個物件,物件充其量也只是儲存對某個函式的參考而已,並非真的「屬於」這個物件,因此,在 JavaScript 中,函式與方法是同義的,並沒有區別。除了在 ES6 新增的 super 參考,super 與 class 一起使用時 super 會靜態綁定函式,經由這樣所綁定的函式就比較接近一般在其他語言所看到的方法了。
陣列使用非負整數作為索引,注意...
const array = [1, 2, 3];
array.length; // 3
array[3] = 4;
array.length; // 4
array['foo'] = 'bar';
array.length; // 4, 陣列的長度不變!
const array = [1, 2, 3];
array['3'] = 'foo';
array // [1, 2, 3, "foo"]
複製物件的方式分為淺拷貝與深拷貝兩種。
為什麼要探討淺拷貝與深拷貝呢?這是根本於基本型別值是傳值而物件是傳參考的緣故,既然物件是傳參考,就要考慮是把整份資料都複製一份,還是複製參考就好?淺拷貝是複製參考,深拷貝是把整份資料都複製一份,常用於考慮物件資料是否要共用的狀況。
除了基本的資料型別中純值(非物件)的資料會真的複製另外一份值之外,其他的都只是複製一份參考而已。例如:Object.assign
在處理超過一層的物件時就只能做到淺拷貝,只有一層的話是可以做到深拷貝的。
複製整個物件,通常會使用 JSON-safe 的物件,先經由序列化為 JSON 字串後再剖析回物件。
const newObj = JSON.parse(JSON.stringify(oldObj));
範例如下。
單層物件時,Object.assign
與「先序列化再剖析」的方法都可以做到完全的拷貝,也就是深拷貝,由於物件的比對的是比較儲存位置,因此當比較拷貝結果時,兩者是不相等的。
const simpleObj = {
a: 1,
b: 2,
};
const newSimpleObj = Object.assign({}, simpleObj);
newSimpleObj === simpleObj // false
const newSimpleObj2 = JSON.parse(JSON.stringify(simpleObj));
newSimpleObj2 === simpleObj // false
那麼,物件再多層的狀況下,又是怎樣的狀況呢?如下,由於 Object.assign
只能做單層的拷貝,因此第二層開始就只是複製參考而已,儲存位置不變,故為 true;而「先序列化再剖析」的方法則是完整地把整個資料複製起來,存到另一個地方,因此儲存位置不同,得到 false。
const obj = {
a: 1,
b: {
c: 2,
d: 3,
}
};
const newObj = Object.assign({}, obj);
newObj.b === obj.b // true
const newObj2 = JSON.parse(JSON.stringify(obj));
newObj2.b === obj.b // false
這篇文章將淺拷貝與深拷貝寫得生動有趣、清楚明暸,歡迎閱讀。
屬性描述器可用來檢視屬性的特徵,例如:可否寫入(writable)、可否配置(configurable)與可否列舉(enumerable)。
例如,檢視 object.a 這個屬性的特徵。
const obj = {
name: 'Apple',
};
Object.getOwnPropertyDescriptor(obj, 'name');
得到結果。
{
value: "Apple",
writable: true,
enumerable: true,
configurable: true,
}
使用 defineProperty 定義物件的屬性與特性。通常使用這種方法的目的是...
範例如下,為物件 obj 定義一個新的屬性 name,並設定其特徵值。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: true,
enumerable: true,
});
obj.name // 'Apple'
屬性的值是否「可被寫入」。
例如,設定 name 這個屬性是不可寫入的,因此當嘗試更新這個值的時候,發現無法更新,並且在 strict mode 之下會丟出 TypeError 的錯誤訊息。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: false, // 不可寫入!
configurable: true,
enumerable: true,
});
obj.name // 'Apple'
obj.name = 'Grape';
obj.name // 'Apple',屬性 name 的值無法被變更!
屬性是否是「可配置的」,意即當 configurable 為 false 的時候,無法再使用 defineProperty 更新特徵的值,否則會丟出 TypeError。但有一個例外,當 configurable 為 false 的時候,writable 仍可由 true 改為 false,但不能從 false 改為 true。
當 configurable 為 false 的時候,無法再使用 defineProperty 更新特徵的值,否則會丟出 TypeError。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: false,
enumerable: true
});
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: true, // false -> true
enumerable: true
});
// Uncaught TypeError: Cannot redefine property: name
當 configurable 為 false 的時候,writable 仍可由 true 改為 false,但不能從 false 改為 true。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: false,
enumerable: true
});
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: false,
configurable: false,
enumerable: true
});
// 這是可行的!
當 configurable 為 false 的時候,writable 仍可由 true 改為 false,但不能從 false 改為 true。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: false,
configurable: false,
enumerable: true
});
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: false,
enumerable: true
});
// Uncaught TypeError: Cannot redefine property: name
除了是否可更新特徵的設定外,configurable 另一個作用就是是否可被 delete 刪除該屬性。
configurable 設為 false,屬性不可刪除。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: false,
enumerable: true,
});
delete obj.name
obj.name // 'Apple',name 屬性未被刪除!
configurable 設為 true,屬性可被刪除。
const obj = {};
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: true,
enumerable: true,
});
delete obj.name
obj.name // undefined
obj // {}
特徵是否為「可列舉的」,例如:此物件的特性是否可在 for...in 中被列舉,若設定 enumerable 為 false 表示不會被列舉出來。
如下,(1) 印出 hello 和 name,(2) 由於 name 被設定為不可列舉的,因此只會印出 hello。
const obj = {};
obj.hello = 'world';
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: true,
enumerable: true,
});
for(let prop in obj) {
console.log(prop); // (1)
}
// hello
// name
Object.defineProperty(obj, 'name', {
value: 'Apple',
writable: true,
configurable: true,
enumerable: false,
});
for(let prop in obj) {
console.log(prop); // (2)
}
// hello
...
...
題外話,會用到 defineProperty 這個東西是為了寫雙向綁定的小工具而追 Vue.js 的原始碼的時候玩到的,推薦閱讀這篇文章。
...
...
如何實作無法被變更的特性或物件?以下會介紹幾種作法,但都只能做到淺層的不可變性(shallow immutability),意即若此物件有指向其他物件的參考,這個被指到的物件的內容就仍是可變的。
如下,假設 foo 是不可變的,但 foo.list 指向一個陣列,這個陣列的內容是可變的。
foo.list = [1, 2, 3];
foo.list.push(4);
foo.list // [1, 2, 3, 4]
使用 defineProperty 設定 writable 為 false 且 configurable 為 false,即可建立一個特性等同於常數的物件屬性,無法被更新、重新定義和刪除。
const obj = {};
Object.defineProperty(obj, 'CONST_PI', {
value: 3.14,
writable: false,
configurable: false,
enumerable: true,
});
obj.CONST_PI // 3.14
使用 Object.preventExtensions
防止物件被加入新屬性。
const obj = {
name: 'Jack',
};
Object.preventExtensions(obj);
obj.hello = 'world';
obj.hello // undefined
備註,在嚴格模式下,加入新屬性會丟出 TypeError。
使用 Object.seal
來達到密封的作用,意即物件不可再新增屬性、重新配置特徵或刪除屬性,但可能可以修改屬性值。
Object.seal
會做兩件事
Object.preventExtensions
防止物件被加入新屬性。const obj = {
name: 'Jack',
};
Object.seal(obj);
// 嘗試加入新的屬性 hello
obj.hello = 'world';
obj.hello // undefined
// 嘗試刪除屬性 name
delete obj.name // false
obj.name // 'Jack'
// 嘗試重新設定特徵值
Object.defineProperty(obj, 'name', {
value: 3.14,
writable: false,
configurable: true,
enumerable: true,
});
// TypeError
使用 Object.freeze
建立一個已凍結的物件,意即這個物件不能新增屬性、更新屬性的值、刪除屬性和重新配置特徵值。
Object.freeze
會做以下的事情
Object.seal
,讓物件不可再新增屬性、重新配置特徵或刪除屬性。回顧前面提到的,以上四種解法都只能做到做到淺層的不可變性(shallow immutability),因此若希望能將整個物件(包含屬性參考的物件)都凍結,可遞迴呼叫 Object.freeze
,但可能有副作用,像是凍結了共用的物件。
const list = ['apple', 'grape'];
const obj = {
name: 'Jack',
favFruits: list,
};
const anotherObj = {
name: 'Apple',
favFruits: list,
};
Object.freeze(obj);
Object.freeze(obj.favFruits);
// 共用的物件被凍結!
list.push('orange'); // Uncaught TypeError: Cannot add property 2, object is not extensible
[[Get]]
[[Get]]
的功用是取得屬性值,例如:obj.a
時會呼叫 [[Get]]()
這個函式呼叫,它會先在此物件內尋找是否有符合名稱的屬性,若無就順著原型串鏈繼續尋找,如果都沒有找到,[[Get]]
就會回傳 undefined。注意,這和之前提到的在語彙範疇中查找變數(的名稱是否被定義)是不同的,若在語彙範疇中找不到該變數,會丟出 ReferrenceError。
[[Put]]
[[Put]]
的功用是...
若此屬性不存在,則新增此屬性並設定其值。但若此屬性存在,則做以下的事情...
備註:上面提到的兩個名詞,這裡來做解釋...
物件預設的 [[Get]]
與 [[Put]]
掌控了屬性的建立、設定和更新、取得值的方式。若要複寫這兩種預設 [[Get]]
與 [[Put]]
行為,可透過取值器與設值器來達成。
方法一,使用物件字面值的方式定義屬性。
const obj = {
get name() {
return this._name_;
},
set name(val) {
this._name_ = `Hi, I am ${val}`;
},
};
obj.name = 'Jack';
obj.name //'Hi, I am Jack'
方法二,使用 defineProperty 的方式定義屬性。
const obj = {};
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
get: function name() {
return this._name_;
},
set: function name(val) {
this._name_ = `Hi, I am ${val}`;
},
});
obj.name = 'Jack';
obj.name //'Hi, I am Jack'
既然屬性不存在的時候,會回傳 undefined,但若屬性值原本就設定為 undefined 是不是就無法判定這個屬性到底存不存在了?
解法是使用 hasOwnProperty,若想進一步確認該屬性是否可在其他物件中找到,可搭配 prop in obj
檢查這個屬性是否存在於原型串鏈中。兩者差異是 prop in obj
會檢查原型串鏈,而 hasOwnProperty 只會檢查該物件。
範例如下。
var obj1 = {
job: undefined,
};
var obj = Object.create(obj1); // 建立 obj 與 obj1 的連結
obj.name = undefined;
屬性 name 真的存在於 obj 嗎?
obj.name // undefined
obj.hasOwnProperty('name'); // true
屬性 name 其值雖然為 undefined,但它真的存在於 obj。
...
...
屬性 job 真的存在於 obj 嗎?
obj.job // undefined
obj.hasOwnProperty('job'); // false
'job' in obj; // true,但在原型串鏈中可找到
obj1.hasOwnProperty('job'); // true
屬性 job 其值雖然為 undefined 且不存在於 obj 中,但可在原型串鏈中可找到,因此進一步檢視 obj1,確定為 obj1 的屬性。
...
...
屬性 hello 真的存在於 obj 嗎?
obj.hello // undefined
obj.hasOwnProperty('hello'); // false
'hello' in obj; // false
雖然 hello 的值是 undefined,似乎與前面的例子無異,但使用 hasOwnProperty 檢視,發現不在 obj 物件中,且經由 prop in obj
確認後發現也無法在原型串鏈中找到,因此屬性不存在。
...
...
總結...
prop in obj
可檢查這個屬性是否可在原型串鏈中找到,讓我們能確認是否需要再往其他物件查找。檢視屬性是否可被列舉的方法。
in 運算子只會帶出可列舉的屬性。
例如,obj 有兩個屬性 name 和 hello,其中 name 為可列舉的,hello 為不可列舉的。
for (let k in obj) {
console.log(k, obj[k]);
}
// 'name', 'Jack'
propertyIsEnumerable 檢視屬性是否可被列舉。
例如,obj 有兩個屬性 name 和 hello,其中 name 為可列舉的,hello 為不可列舉的。檢視 name 是否為可列舉的,會回傳 true。
obj.propertyIsEnumerable('name') // true
Object.keys
vs Object.getOwnPropertyNames
Object.keys
與 Object.getOwnPropertyNames
都只回傳此物件的屬性,且皆不檢視原型串鏈,兩者差異在於
Object.keys
:回傳所有可列舉的屬性。Object.getOwnPropertyNames
:回傳所有屬性,不管是否可被列舉。例如,obj 有兩個屬性 name 和 hello,其中 name 為可列舉的,hello 為不可列舉的。
Object.keys(obj); // ['name']
Object.getOwnPropertyNames(obj); // ['name', 'hello']
迭代出陣列的值的方法。
迭代陣列中所有的值。
const list = ['Apple', 'Bob', 'Cathy', 'Doll'];
list.forEach((item, index, array) => {
console.log(item, index, array);
});
檢查陣列中的每個值是否符合條件,若是則回傳 true。持續進行直到結束,或 callback 中回傳 false 就停止迭代。
const list = [
{
name: 'apple',
count: 20,
},
{
name: 'corn',
count: 100,
},
{
name: 'grape',
count: 50,
},
{
name: 'pineapple',
count: 80,
},
];
const result = list.every((item, index, array) => {
console.log(item, index, array);
return item.count > 50;
});
console.log(`result: ${result}`);
檢查陣列中的是否有值符合條件,若是則回傳 true。持續進行直到結束,或 callback 中回傳 true 就停止迭代。
const list = [
{
name: 'apple',
count: 20,
},
{
name: 'corn',
count: 100,
},
{
name: 'grape',
count: 50,
},
{
name: 'pineapple',
count: 80,
},
];
const result = list.some((item, index, array) => {
console.log(item, index, array);
return item.count > 50;
});
console.log(`result: ${result}`);
若想看更多陣列處理方法,可參考這裡
for...of
使用 ES6 的 for...of
迭代陣列。
const array = [1, 2, 3];
for (let v of array) {
console.log(v);
}
// 1
// 2
// 3
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
[ ]
的方法存取。Object.assign
只能處理一層內的深拷貝,而真正的深拷貝通常是由 JSON-safe 的物件先經由序列化為 JSON 字串後再剖析回物件來達成。[[Get]]
與 [[Put]]
掌控了屬性的建立、設定和更新、取得值的方式,若要複寫這兩種預設 [[Get]]
與 [[Put]]
行為,可透過取值器與設值器來達成。prop in obj
可判斷屬性是否存在。Object.keys
與 Object.getOwnPropertyNames
都只回傳此物件的屬性,差異在於前者只列出可列舉的屬性,而後者會列出所有此物件的屬性。for...of
。同步發表於部落格。